﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;
using PhiStore.ObjectManaging;
using PhiStore.Packages;
using PhiStore.Packages.InternalPackages;
using PhiStore.Security;
using PhiStore.Threading;

namespace PhiStore.Communication
{
    internal class PeerWorker : Boostable
    {
        private Channel<ForwardedPackage> inputChannel;
        TcpClient client;
        AesCryptoServiceProvider aes;
        BinaryFormatter formatter;
        Stream outputStream;
        Stream inputStream;
        BroadcastObject ExternalConnection = null;

        public PeerContainer Peer = null;

        public PeerWorker()
        {
            inputChannel = new Channel<ForwardedPackage>(50);
            formatter = new BinaryFormatter();
        }

        private ReturnCode Connect(IPAddress ip, int port)
        {
            aes = null;
            client = new TcpClient();
            client.Connect(new IPEndPoint(ip, port));
            client.SendTimeout = 5000;
            client.ReceiveTimeout = 5000;

            inputStream = client.GetStream();
            outputStream = client.GetStream();

            //Fetch server's public key
            SendRequest(new WhoAreYou());
            BroadcastObject bc = ReceiveReply<BroadcastObject>();

            aes = new AesCryptoServiceProvider();
            aes.Key = Helper.GenerateKey(256);
            aes.GenerateIV();

            //log in to the server
            Connect conn = new Connect(CredentialManager.Instance.GetServerCredential(), aes.Key, aes.IV);
            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
            rsa.ImportParameters(bc.PublicKey);
            RSAEncrypted encrypted = Encryption.RSAEncrypt(conn, rsa, formatter);

            SendRequest(encrypted);

            IPAddress correctIP = IPAddress.IsLoopback(ip) ? bc.IP : ip;
            ExternalConnection = new BroadcastObject(bc.PublicKey, correctIP, port); //storing info for reconnects
            ReturnCode result = ReceiveReply<ReturnCode>();

            return result;
        }

        //return success / failure
        public ReturnCode ConnectAndLaunch(IPAddress ip, int port)
        {
            try
            {
                ReturnCode result = Connect(ip, port);

                if (result != ReturnCode.Success)
                {
                    Log.Instance.AddLogEntry(ExternalConnection.ToString(), "Unable to connect due to; " + result.ToString());
                    return result;
                }
                else
                {
                    PeerContainer pc = new PeerContainer(ExternalConnection, inputChannel);

                    if (!PeerManager.Instance.Exist(pc)) //are we already connected?
                    {
                        SendRequest(new WhoAmI());
                        IPAddress MyIP = ReceiveReply<IPAddress>();

                        if (PhiServer.Instance.UpdateBroadcastObject(MyIP))
                        {
                            //change IP for all peers connected.
                            List<PeerContainer> peers = PeerManager.Instance.GetPeers();
                            foreach (PeerContainer p in peers)
                            {
                                p.PeerWorkerChannel.Send(
                                    new ForwardedPackage(new WhoIAm(MyIP), null)
                                    );
                            }
                        }

                        //fetch credentials from external server
                        SendRequest(new ListCredentials());
                        IEnumerable<PhiCredentials> creds = ReceiveReply<IEnumerable<PhiCredentials>>();
                        foreach (PhiCredentials cred in creds)
                        {
                            CredentialManager.Instance.AddCredentials(cred);
                        }

                        //fetch objects from external server
                        SendRequest(new ListObjects());
                        List<SimpleObjectEntry> objs = ReceiveReply<List<SimpleObjectEntry>>();

                        foreach (SimpleObjectEntry entry in objs)
                        {
                            PhiServer.Instance.ObjectManagerChannel.Send(new AddRemoteObject(pc, entry));
                        }

                        Peer = pc;
                        //PeerManager.Instance.AddPeer(pc);

                        //launch self
                        Booster.Boost(this);
                        return result;
                    }
                    else
                        return ReturnCode.Failure;
                   

                }

            }
            catch (Exception e)
            {
                Log.Instance.AddLogEntry("PeerCommunication","Unable to connect due to; " + e.ToString());
                return ReturnCode.Failure;
            }
        }

        public ReturnCode ConnectAndLaunch(string host, int port)
        {

            IPAddress ip = IPAddress.None;
            if (!IPAddress.TryParse(host, out ip)) //is host an IP
            {
                try
                {
                    IPHostEntry entry = Dns.GetHostEntry(host);
                    if (entry.AddressList.Length > 0)
                        ip = entry.AddressList[0];
                }
                catch (SocketException)
                {
                    ip = IPAddress.None;
                    //host not found
                }
            }

            if (ip == IPAddress.None)
            {
                Log.Instance.AddLogEntry("PeerCommunicator","Unable to resolve " + host);
                return ReturnCode.Failure;
            }
            if (ip.Equals(IPAddress.IPv6Loopback))
            {
                ip = IPAddress.Loopback;
            }


            Log.Instance.AddLogEntry("PeerCommunicator", "Host name translated to: " + ip.ToString());

            return ConnectAndLaunch(ip, port);
        }

        private void SendRequest(PhiPackage action)
        {
            PhiPackage toSend = action;
            if (aes != null && !(action is RSAEncrypted))
                toSend = Encryption.Encrypt(toSend, aes, formatter);

            formatter.Serialize(outputStream, toSend);
            outputStream.Flush();
        }

        private T ReceiveReply<T>()
        {
            object reply = formatter.Deserialize(inputStream);
            if (reply is Encrypted)
                reply = Encryption.Decrypt((Encrypted)reply, aes, formatter);

            return (T)reply;
        }

        public void Run()
        {
            Log.Instance.AddLogEntry(ExternalConnection.ToString(), "Peer worker started");
            ForwardedPackage action = null;
            try
            {
               
                while (true)
                {
                    try
                    {
                        if (action == null)
                        {
                            action = inputChannel.Receive();
                            Log.Instance.AddLogEntry(ExternalConnection.ToString(), "Forwarding " + action.Action.ToString());
                        }

                        PhiPackage innerAction = action.Action;
                        SendRequest(innerAction);

                        object result = ReturnCode.WorkingOnObject;
                        while(ReturnCode.WorkingOnObject.Equals(result))
                        {
                            result = ReceiveReply<object>();

                            if (action.ReplyChannel != null)
                            {
                                action.ReplyChannel.Send(result);
                            }
                        }

                        //package completed
                        action = null;
                    }
                    catch (Exception ex)
                    {
                        if (ex is PoisonException) //shut down
                        {
                            throw ex;
                        }

                        Log.Instance.AddLogEntry(ExternalConnection.ToString(), "Peer Timeout - reconnecting " + ex.ToString());
                        if (ReturnCode.Success != Connect(ExternalConnection.IP, ExternalConnection.Port))
                        {
                            if (action.ReplyChannel != null)
                                action.ReplyChannel.Poison();
                            throw new WebException("Connection Lost");
                        }
                        else
                        {
                            SendRequest(new AddPeer(PhiServer.Instance.BroadcastObject.IP, PhiServer.Instance.BroadcastObject.Port));
                            ReceiveReply<ReturnCode>();
                        }
                    }
                    
                }
            }
            catch (PoisonException)
            {
                Log.Instance.AddLogEntry(ExternalConnection.ToString(), "Peer worker stopped");
                if (action != null && action.ReplyChannel != null)
                {
                    action.ReplyChannel.Poison();
                }
            }
            catch (Exception e) //?
            {
                Log.Instance.AddLogEntry(ExternalConnection.ToString(), "Connection to peer lost; " + e.ToString());
                if (action != null && action.ReplyChannel != null)
                {
                    this.inputChannel.Poison();
                    action.ReplyChannel.Send(ReturnCode.ConnectionLost);
                    //calling method removes the peer
                }
                else
                {
                    //gossip is maintained by peermanager.
                    this.inputChannel.Poison();

                    PeerManager.Instance.RemovePeer(new PeerContainer(ExternalConnection, inputChannel));
                }
                
            }

            while (this.inputChannel.Count > 0) //CLEAR INPUT CHANNEL
            {
                ForwardedPackage fa = inputChannel.Receive();
                if (fa.ReplyChannel != null)
                    fa.ReplyChannel.Send(ReturnCode.ConnectionLost);
            }
        }

    }
}
